/*
 * File ChatNi.java
 */
package cs.ni;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.util.Observable;
import java.util.Observer;
import java.util.logging.Level;

import cs.controller.Controller;
import cs.entity.User;
import cs.entity.UserFile;
import cs.entity.UserTextMessage;
import cs.main.ChatSystem;
import cs.model.FileToSendModel;
import cs.model.MsgToSendModel;
import cs.model.ReceivedFileModel;
import cs.model.State;
import cs.model.State.LocalUserState;
import cs.remoteapp.FileAnswer;
import cs.remoteapp.FileRequest;
import cs.remoteapp.Goodbye;
import cs.remoteapp.Hello;
import cs.remoteapp.Message;
import cs.remoteapp.TextMessage;

/**
 * The chat network interface
 * represents the network manager.<br> It's used for 
 * sending/receiving data using the network.<br>
 * Uses observer pattern to get updates of sending requests.<br>
 * Only the hello message is not using the Oberver/Observable relation. <p>
 * Package  cs.ni
 * Project ChatSystemG1 - INSA 4INFOA1 - 2013/2014
 * @author Julie DUMONS & Juste OUEDRAOGO
 */
public class ChatNi  implements Observer, RemoteApp {
	
	/**
	 * listening socket for tcp incoming connections 
	 * listens on tcp port (tcpListeningPort) for incoming connection requests
	 */
	private  ServerSocket servSocket;
	
	/**
	 * udp socket for sending messages including hello, bye, ...
	 */
	private  DatagramSocket udpSendSocket;
	
	/**
	 * udp socket for receiving messages
	 * listen on udp port (udpListeningPort) for incoming messages
	 */
	private DatagramSocket udpRecvSocket;
	
	/**
	 * object responsible for receiving data over the network
	 */
	private ChatNiReceiver receiver;
	
	/**
	 * object responsible for sending messages overt network using udp
	 */
	private static UdpSender udpSender;
	
	/**
	 * controller of the chat system
	 */	
	private static Controller controller;
	
	/**
	 * udp server port
	 */
	private int udpPort;
	
	/**
	 * tcp server port
	 */
	private int tcpPort;
	
	/**
	 * constructor
	 */
	public ChatNi(int udpListeningPort, int tcpListeningPort){	
		udpPort = udpListeningPort;
		tcpPort = tcpListeningPort;
	}
		
	
	/**
	 * Called when the model has changed
	 */
	@Override
	public void update(Observable arg0, Object arg1) {
		
		/*
		 * connection and disconnection (triggered by ChatGui)
		 */
		if (arg0 instanceof State) {
			if ((LocalUserState)arg1 == LocalUserState.ONLINE) {
				
				//open listening ports for tcp and udp sockets
				initSockets();
				
				//can now send messages
				initSenders();
				
				//can now receive messages
				initReceiver();
				
				// inform user
				System.out.println("performing connect... ");
				
				// sending in broadcast
				for(InetAddress addr :controller.getLocalUser().getBroadcastAddr()){
					sendHello(addr, true);
					System.out.println("hello sent to "+addr);
				}
				System.out.println();
				
			} else if ((LocalUserState)arg1 ==  LocalUserState.OFFLINE) {

				// inform user
				System.out.println("performing disconnect ( Send Goodbye to everyone...) ");
				sendGoodbye() ; 
				//close all socket and stop receiver threads
				closeSockets();
				
			} 
				
		}
		
		/*
		 * case new message to send
		 */
		else if(arg0 instanceof MsgToSendModel){
			sendTextMessage();
		}
		
		
		/**
		 * case file to send
		 */
		else if(arg0 instanceof FileToSendModel){
			
			UserFile fileToSend = (UserFile)arg1;
			 
			if(fileToSend.getState().equals(UserFile.State.REQUEST)){
				sendFileRequest(fileToSend);
				System.out.println("file request sent");
			}
			else if(fileToSend.getState().equals(UserFile.State.ACCEPTED)){
				sendFile(fileToSend);
				System.out.println("sending file...");
			}
			else{
				System.out.println("file "+fileToSend.getState());
			}
			
		}
		
		
		/*
		 * case file answer to send
		 */
		//TODO send file answer
		else if(arg0 instanceof ReceivedFileModel){
			UserFile filetoDownload = (UserFile)arg1;
			//a file request have been accepted by the local user
			if(filetoDownload.getState().equals(UserFile.State.ACCEPTED))
				sendFileAnswer(filetoDownload);
		}
		
		//download completed
		else if(arg0 instanceof FileReceiverHandler){
			controller.performDownloadComplete((UserFile)arg1);
		}
		
		//upload completed
		else if(arg0 instanceof FileSender){
			
			controller.performUploadComplete((UserFile)arg1);
		}
		
		//Message received from udp port 
		else if(arg0 instanceof UdpReceiver){
			try {
				 // get the received message
				DatagramPacket packet = (DatagramPacket)arg1;
				ByteArrayInputStream in = new ByteArrayInputStream(packet.getData());
				ObjectInputStream input = new ObjectInputStream(in);
				Object obj =  input.readObject();
				if(obj instanceof Message){
					processReceive((Message)obj, packet.getAddress());
				}
			} catch (IOException e) {;
				ChatSystem.logger.log(Level.WARNING, "IO Exeception\n");

			} catch (ClassNotFoundException e) {
				// TODO Auto-generated catch block
				ChatSystem.logger.log(Level.WARNING, "ClassNotFound Exeception\n");

			}
			
		}
		
	}

	/**
	 * Instantiation of a new ChatNiReceiver
	 */
	private void initReceiver(){
		//start receiver
		if(receiver == null && udpRecvSocket!=null && servSocket!=null)
			receiver = new ChatNiReceiver(this, udpRecvSocket, servSocket);
	}
	
	private void initSenders(){
		if(udpSendSocket !=null)
			udpSender = new UdpSender(udpSendSocket);
	}
	/**
	 * initialize udp and tcp sockets
	 */
	private void initSockets(){
		try {
			servSocket = new ServerSocket(tcpPort);
			udpRecvSocket = new DatagramSocket(udpPort);
			udpSendSocket = new DatagramSocket();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			controller.performNetworkError();
			Thread.currentThread().stop();
		}
		
	}
	
	/**
	 * close udp and tcp sockets 
	 */
	private void closeSockets(){
		try {
			receiver.stopReceiving();
			receiver = null;
			servSocket.close();
			udpRecvSocket.close();
			udpSendSocket.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			ChatSystem.logger.log(Level.WARNING, "IO Exeception");
		}
		
	}
	
	
	/**
	 * set the controller
	 * @param ctrl
	 */
	public void setController(Controller ctrl){
		controller = ctrl;
	}
	
	/**
	 * get the controller
	 * @return controller
	 */
	public Controller  getController() {
		// TODO Auto-generated method stub
		return controller;
	}


	@Override
	public void sendHello(InetAddress addr, boolean isBroadcast) {
		Hello hello = new Hello(controller.getLocalUser().getUsername());
		hello.setBroadcast(isBroadcast);
		udpSender.send(hello, addr, udpPort);	
	}


	@Override
	public void sendGoodbye() {
		Goodbye bye = new Goodbye(controller.getLocalUser().getUsername());
		for(InetAddress addr : controller.getLocalUser().getBroadcastAddr())
			udpSender.send(bye, addr, udpPort);
		
	}

	

	@Override
	public void sendTextMessage() {
		//get the message to send
		UserTextMessage msg = controller.getMessageToSend();	
		TextMessage msgToSend = new TextMessage(controller.getLocalUser().getUsername(), msg.getMessage());
		udpSender.send(msgToSend, msg.getRemoteUser().getIpAddress(), udpPort);
		
	}

	
	@Override
	public void sendFileRequest(UserFile file) {
		FileRequest req = new FileRequest(controller.getLocalUser().getUsername(),
								file.getFile().getName(), file.getFile().length());
		
		udpSender.send(req, file.getRemoteUser().getIpAddress(), udpPort);
		
	}

	
	@Override
	public void sendFileAnswer(UserFile file) {
		boolean accepted = (file.getState() == UserFile.State.ACCEPTED) ? true : false;
		FileAnswer answ = new FileAnswer(controller.getLocalUser().getUsername(), file.getName(), accepted);
		udpSender.send(answ, file.getRemoteUser().getIpAddress(), udpPort);
		
	}

	
	@Override
	public void sendFile(UserFile f) {
		UserFile file = controller.getFileTosend(f);
		System.out.println(file.getSize());
		FileSender sender = new FileSender(file, file.getRemoteUser().getIpAddress(), tcpPort);
		sender.addObserver(this);
		Thread uploader = new Thread(sender);
		uploader.start();
	}
	
	
	@Override
	public void processReceive(Message msg, InetAddress addr) {
		/*
		 * case hello message
		 */
		if(msg instanceof Hello){
			Hello hello = (Hello) msg;
			
			// don't consider messages received from the localUser
			if (!controller.getLocalUser().getIpAddrList().contains(addr)) {
				System.out.println("Receiving hello from: "+addr);
				//add a new user to the userList
				controller.performAddUser(new User(hello.getUsername(), addr));
				//if the Hello message has been broadcasted send a Hello message back
				if( hello.isBroadcast()){
					sendHello(addr, false);
				}
			}
			
		}
		
		/*
		 *  case bye message
		 */
		else if (msg instanceof Goodbye){
			
			if (!controller.getLocalUser().getIpAddrList().contains(addr)) {
				System.out.println("Receiving bye from: "+addr);
				controller.performRemoveUser(new User(msg.getUsername(), addr));
			}
			
		}
		
		/*
		 * case TextMessage
		 */
		
		else if(msg instanceof TextMessage){
			TextMessage text = (TextMessage)msg;
			System.out.println("Receiving text message from: "+addr);
			User remoteuser = new User(text.getUsername(), addr);
			controller.performAddReceivedMsg(new UserTextMessage(remoteuser, text.getText()));
		}
		
		/*
		 * receiving a FileRequest
		 */
		else if(msg instanceof FileRequest){
			FileRequest req = (FileRequest)msg;
			System.out.println("Receiving file request from: "+addr);
			User remoteuser = new User(req.getUsername(), addr);
			UserFile file = new UserFile(remoteuser, new File(req.getFileName()));
			//file.setName(req.getFileName());
			file.setSize(req.getFileSize());
			controller.performReceivedFileRequest(file);
		}
		
		
		/*
		 * receiving a  FileAnswer (response to a file request)
		 */
		else if(msg instanceof FileAnswer){
			FileAnswer answ = (FileAnswer)msg;
			System.out.println("Receiving file answer from: "+addr);
			User remoteuser = new User(answ.getUsername(), addr);
			UserFile file = new UserFile(remoteuser, new File(answ.getFileName()));
			//file.setName(answ.getFileName());
			UserFile.State state = answ.isAccepted() ? UserFile.State.ACCEPTED : UserFile.State.REFUSED;
			file.setState(state);
			controller.performReceivedFileAnwser(file);
		}

		
	}

	
}